﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;

namespace NN
{
    public class GeneticAlgorithm
    {
        public double scaleMax;
        public double scaleMin;

        public char nnFunction { get; set; }
        public MainWindow ParentWindow { get; set; }

        public int popSize { set; get; }
        public double mu { set; get; }

        public List<double> aam;
        public List<double> correlation;

        public double genAlgTrainingTime;
        public double latestValidationError;
 
        public GeneticAlgorithm(int PopulationSize, double MutationRate, int[] Layers,
            double[,] Inputs, double[,] Outputs, double[,] ValInputs, double[,] ValOutputs,
            char ConvergenceOption, double ConvergenceParameter)
        {
            popSize = PopulationSize;
            mu = MutationRate;
            layers = Layers;
            inputs = Inputs;
            outputs = Outputs;            

            if (ValInputs != null && ValOutputs != null)
            {
                valInputs = ValInputs;
                valOutputs = ValOutputs;
                scale(ref valInputs, 1);                
                useValidationSet = true;
            }
            else
            {
                useValidationSet = false;
            }


            scale(ref inputs, 1);            
            rand = new Random();

            nnFunction = 't';

            epochs = 0;
            latestValidationError = -1;
            scaleMin = double.MinValue;
            scaleMax = double.MaxValue;
            convergenceParameter = ConvergenceParameter;
            convergenceOption = ConvergenceOption;
            converged = false;
            abort = false;
            ParentWindow = new MainWindow();
        }

        public void SetLayers(int[] l)
        {
            layers = l;
        }

        public void SetWeights(double[][][] w)
        {
            for(int i = 0; i < popSize; i++)
            {
                genomes[i].SetWeights(w);                
            }
        }
        public void SetBiases(double[][] b)
        {
            for (int i = 0; i < popSize; i++)
            {
                genomes[i].SetBiases(b);
            }            
        }

        // scales data array to [-scale, +scale]
        public void scale(ref double[,] data, int scale)
        {
            double max = double.MinValue;
            double min = double.MaxValue;

            scaleMax = max;
            scaleMin = min;

            for (int i = 0; i < data.GetLength(0); i++)
                for (int j = 0; j < data.GetLength(1); j++)
                    if (data[i, j] > max)
                        max = data[i, j];
                    else if (data[i, j] < min)
                        min = data[i, j];

            for (int i = 0; i < data.GetLength(0); i++)
                for (int j = 0; j < data.GetLength(1); j++)
                    data[i, j] = (((data[i, j] - min) / (max - min)) * (2 * scale)) - scale;
        }

        // Main GA loop -> calculates errors/fitness, breeds, mutates
        public void runGA()
        {
            initGenomes();                        
            abort = false;

            timer = new Stopwatch();
            timer.Start();
            while (!converged && !abort)
            {
                updateFitnesses();
                checkConvergence();
                if (!converged)
                {
                    breed();
                    mutate();                    
                }
                epochs++;

                ParentWindow.GAErrorChanged();
            }
            sortPopulation();
        }

        // Calculuates the length of the data, inputs and outputs and total
        // Create the population of genomes
        private void initGenomes()
        {
            genomes = new Genome[popSize];
            offsprings = new Genome[popSize];            

            for (int i = 0; i < popSize; i++)
            {
                genomes[i] = new Genome(layers, mu, rand, nnFunction);
                offsprings[i] = new Genome(layers, mu, rand, nnFunction);
            }
        }

        // convergence option = 1: converges when average error is below convergenceParameters
        //                    = 2: converges when average angle measure is above convergenceParameter
        //                    = 3: converges when correlation is above convergenceParameter
        //                    = 4: converges when validation set error starts increasing
        private void checkConvergence()
        {
            // convergence based on average error
            if (convergenceOption == '1' && (genomes[0].error / inputs.GetLength(0) < convergenceParameter))
            {
                if (useValidationSet)
                {
                    converged = this.validateConvergence();
                }
                else
                {
                    converged = true;
                }

                if (converged)
                {                    
                    timer.Stop();
                    this.genAlgTrainingTime = (int)timer.ElapsedMilliseconds / 1000;
                    //invoke(backPropCompletedDelegate);
                    ParentWindow.EndGenAlg();
                }
            }
            // convergence based on average angle measure
            else if (convergenceOption == '2' && (Math.Abs(genomes[0].aam) > convergenceParameter))
            {
                converged = true;
            }
            // convergence based on correlation value
            else if (convergenceOption == '3' && (Math.Abs(genomes[0].correlation) > convergenceParameter))
            {
                converged = true;
            }
        }

        // Calculate the fitnesses for the population
        private void updateFitnesses()
        {
            for (int i = 0; i < popSize; i++)
            {
                genomes[i].updateFitness(inputs, outputs, valInputs, valOutputs);
            }
        }

        // Breed the network using the roulette wheel algorithm
        private void breed()
        {
            sortPopulation();
            normalizeFitnessValues();
            createOffsprings();
        }

        // Bubble sort the population fitnesses largest to smallest
        private void sortPopulation()
        {
            bool swapped;
            Genome temp = new Genome();

            do
            {
                swapped = false;
                for (int i = popSize - 1; i > 0; i--)
                {
                    for (int j = 0; j < i; j++)
                    {
                        if (genomes[j].fitness < genomes[j + 1].fitness)
                        {
                            temp = genomes[j + 1];
                            genomes[j + 1] = genomes[j];
                            genomes[j] = temp;
                            swapped = true;
                        }
                    }
                }
            } while (swapped == true);
        }

        // make sum of fitnesses == 1
        private void normalizeFitnessValues()
        {
            double fitnessSum = 0;

            // Sum up the total fitness values
            for (int i = 0; i < popSize; i++)
                fitnessSum += genomes[i].fitness;

            // Normalize the values so that sum(fitnesses) == 1
            for (int i = 0; i < popSize; i++)
                genomes[i].normFitness = genomes[i].fitness / fitnessSum;

            // Calculate the accumulated normalized fintess values for each genome
            genomes[0].accNormFitness = genomes[0].normFitness;
            for (int i = 1; i < popSize; i++)
                genomes[i].accNormFitness = genomes[i].normFitness + genomes[i - 1].accNormFitness;
        }

        // Breed the new population
        private void createOffsprings()
        {
            for (int i = 1; i < popSize; i++)
                createOffspring(selectBreeder(), selectBreeder(), i);

            for (int i = 1; i < popSize; i++)
                genomes[i].decGenome = offsprings[i].decGenome;
        }

        // Select breeder based on roulette wheel and normalized fitnesses
        private int selectBreeder()
        {
            double val = rand.NextDouble();

            for (int i = 0; i < popSize - 1; i++)
                if (val <= genomes[i].accNormFitness)
                    return i;

            return popSize - 1;
        }

        // Breed using two point crossover based on NN weights
        private void createOffspring(int a, int b, int n)
        {
            for (int i = 0; i < genomes[a].decGenomeLength; i++)
            {
                if (rand.NextDouble() < 0.5)
                    offsprings[n].decGenome[i] = genomes[a].decGenome[i];
                else
                    offsprings[n].decGenome[i] = genomes[b].decGenome[i];
            }
        }

        // Mutate the genomes' binary string->converts to dec then weights
        private void mutate()
        {
            for (int i = 1; i < popSize; i++)
                genomes[i].mutate();
        }

        public double getFitnessOfBestGenome()
        {
            return genomes[0].fitness;
        }

        public double getErrorofBestGenome(int epoch)
        {
            if (epoch > 0)
            {
                return genomes[0].error;
            }
            else
            {
                return -1;
            }
        }
        public double[][][] GetWeights()
        {            
            return genomes[0].GetWeights();
        }
        public double[][] GetBiases()
        {
            return genomes[0].GetBiases();
        }

        public double GetAAM(int epoch)
        {
            if (epoch > 0)
            {
                return genomes[0].aam;
            }
            else
            {
                return -1;
            }
        }

        public double GetCorrelation(int epoch)
        {
            if (epoch > 0)
            {
                return Math.Abs(genomes[0].correlation);
            }
            else
            {
                return -1;
            }
        }

        public int GetElapsedEpochs()
        {
            return epochs;
        }

        private bool validateConvergence()
        {
            double[] tempOut = new double[outputs.GetLength(1)];
            double[] tempIn = new double[inputs.GetLength(1)];
            double partialError = 0, totalError = 0;
            bool isConverged;

            //run network against validation set
            for (int i = 0; i < valInputs.GetLength(0); i++)
            {
                for (int j = 0; j < valInputs.GetLength(1); j++)
                {
                    tempIn[j] = valInputs[i, j];
                }

                tempOut = genomes[0].Fprop(tempIn);

                for (int j = 0; j < valOutputs.GetLength(1); j++)
                {
                    partialError += Math.Pow(valOutputs[i, j] - tempOut[j], 2);
                }

                totalError += partialError;
            }

            totalError *= 0.5;

            latestValidationError = totalError;
            totalError /= inputs.GetLength(0);            

            if (totalError < convergenceParameter)
            {
                isConverged = true;
            }
            else
            {
                isConverged = false;
            }

            return isConverged;
        }

        public double[] Fprop(double[] Inputs)
        {
            double[] theOutputs;

            sortPopulation();
            theOutputs = genomes[0].Fprop(Inputs);

            return theOutputs;
        }

        public void abortTraining()
        {
            abort = true;
        }

        const int WEIGHT_SCALE = 512;
        const int INPUTS_SCALE = 100;

        public int epochs;        
        int[] layers;

        public Genome[] genomes;
        Genome[] offsprings;

        double[,] inputs;
        double[,] outputs;
        double[,] valInputs;
        double[,] valOutputs;

        bool converged;
        bool abort;
        bool useValidationSet;


        Random rand;
        Stopwatch timer;        

        public double convergenceParameter;        
        char convergenceOption;
    }
}